很多人第一次看 LINE plugin,會直覺地說:
「這不就跟 Telegram 一樣嗎?不就是另一個聊天平台?」
表面上看起來真的很像。
都是 bot、都是 webhook、都是收到訊息後交給 agent。
但你一旦真的把它們接進同一個系統,就會發現兩邊其實很不一樣:
第 23 天我想講的重點是:
LINE plugin 不是 Telegram 的複製貼上,它是另一種入口,但接的是同一顆核心。
這篇會先看 LINE 怎麼被裝進 OpenClaw,再看它為什麼要把 reply payload 翻成 LINE 的 rich message 語言。
LINE 的接法我會切成四層:
如果說 Telegram 比較像「強化過的聊天入口」,那 LINE 比較像「先翻譯、再投遞」。
因為 LINE 的介面語言跟 OpenClaw 的內部 reply payload 並不完全同型。
先看入口。
📄 原始碼:
extensions/line/index.ts:3-50
import {
defineBundledChannelEntry,
type OpenClawPluginApi,
} from "openclaw/plugin-sdk/channel-entry-contract";
export default defineBundledChannelEntry({
id: "line",
name: "LINE",
description: "LINE Messaging API channel plugin",
importMetaUrl: import.meta.url,
plugin: {
specifier: "./api.js",
exportName: "linePlugin",
},
runtime: {
specifier: "./runtime-api.js",
exportName: "setLineRuntime",
},
registerFull(api) {
api.registerCommand({
name: "card",
description: "Send a rich card message (LINE).",
acceptsArgs: true,
requireAuth: false,
async handler(ctx) {
const command = await loadLineCardCommand(api);
return await command.handler(ctx);
},
});
},
});
這裡已經可以看出 LINE 的個性。
它不只是把 plugin 載入,還順手註冊了 /card 指令。
這代表 LINE 在 OpenClaw 裡不是只負責收發文字,它還有一組 rich message 的操作語法。
再看它怎麼接 core。
📄 原始碼:
extensions/line/src/channel.ts:41-102
export const linePlugin: ChannelPlugin<ResolvedLineAccount> = createChatChannelPlugin({
base: {
id: "line",
...lineChannelPluginCommon,
setupWizard: lineSetupWizard,
groups: {
resolveRequireMention: resolveLineGroupRequireMention,
},
messaging: {
normalizeTarget: (target) => {
const trimmed = target.trim();
if (!trimmed) {
return undefined;
}
return trimmed.replace(/^line:(group|room|user):/i, "").replace(/^line:/i, "");
},
resolveInboundConversation: lineBindingsAdapter.resolveInboundConversation,
transformReplyPayload: ({ payload }) => {
if (!payload.text || !hasLineDirectives(payload.text)) {
return payload;
}
return parseLineDirectives(payload);
},
targetResolver: {
looksLikeId: (id) => {
const trimmed = id?.trim();
if (!trimmed) {
return false;
}
return /^[UCR][a-f0-9]{32}$/i.test(trimmed) || /^line:/i.test(trimmed);
},
hint: "<userId|groupId|roomId>",
},
},
directory: createEmptyChannelDirectoryAdapter(),
setup: lineSetupAdapter,
status: lineStatusAdapter,
gateway: lineGatewayAdapter,
bindings: lineBindingsAdapter,
conversationBindings: {
defaultTopLevelPlacement: "current",
},
},
});
這段最值得記住的是 transformReplyPayload。
它說明 LINE 不只是把 OpenClaw 回覆原封不動送出去,而是要先看文字裡有沒有 LINE directives,然後把它變成 LINE 可以理解的 rich message 結構。
Telegram 很多時候可以比較直接地把訊息送出去。
LINE 不一樣。
它很重視 rich message 形式,所以 OpenClaw 在產出 reply 之後,還要再做一次轉譯:
這些不是裝飾,而是 LINE UX 的核心語言。
transformReplyPayload 是 LINE 的關鍵分水嶺這個 hook 很漂亮。
它讓 OpenClaw core 不用知道 LINE 的全部視覺規則,只要照著自己的方式產出 payload。
到了 LINE plugin 這層,再把特殊 directive 翻成 rich message。
這種設計有一個很大的好處:
你看 normalizeTarget 和 looksLikeId 就會發現,LINE 很在意 target 的格式。
它要接受:
line:user:...
line:group:...
line:room:...
這代表 LINE plugin 不只是送訊息,它還在幫 OpenClaw 把各種 user-facing 輸入整理成正規化資料。
Telegram 常見的是:
LINE 常見的是:
所以如果你硬把 Telegram 的想法原封不動搬來 LINE,會很怪。
OpenClaw 的做法是:
這才是正確的複用。
但這就是 channel plugin 的價值。
不是把一切做成一樣,而是把共通部分保留,差異部分封裝。
defineBundledChannelEntry 讓 OpenClaw 找得到這個 plugincreateChatChannelPlugin 把 LINE 接進 coretransformReplyPayload 是 LINE rich message 能力的關鍵第 24 天我要接著看 LINE 的事件、驗證與常見坑。
因為 LINE 的入口很講究 raw body 和 signature,這裡一旦錯,後面整條流程就不會開始。